import os
import sys
from typing import Any, Callable, NoReturn, Optional, Tuple

__all__ = ['round', 'ceil', 'floor', 'overload', 'to_fullwidth',
           'to_halfwidth', 'readchar', 'clear_screen', 'get_position', 'numeral_input']


def round(n: int, start: int = 0, step: int = 10) -> int:
    """Round the number `n` to the nearest `step` that begins from
    `start`.

    Args:
        - `n` (`int`): The number to round.
        - `start` (`int`, optional): The start number. Defaults to 0.
        - `step` (`int`, optional): The step to which to round the number.
        Defaults to 10.

    Returns:
        - `int`: The rounded number.
    """
    x = (n - start) // step * step + start
    if n - x >= step / 2:
        x += step
    return x


def ceil(n: int, start: int = 0, step: int = 10) -> int:
    """Round the number `n` to the nearest `step` upwards that begins from
    `start`.

    Args:
        - `n` (`int`): The number to round.
        - `start` (`int`, optional): The start number. Defaults to 0.
        - `step` (`int`, optional): The step to which to round the number.
        Defaults to 10.

    Returns:
        - `int`: The rounded number.
    """
    x = (n - start) // step * step + start
    if n - x:
        x += step
    return x


def floor(n: int, start: int = 0, step: int = 10) -> int:
    """Round the number `n` to the nearest `step` downwards that begins
    from `start`.

    Args:
        - `n` (`int`): The number to round.
        - `start` (`int`, optional): The start number. Defaults to 0.
        - `step` (`int`, optional): The step to which to round the number.
        Defaults to 10.

    Returns:
        - `int`: The rounded number.
    """
    x = (n - start) // step * step + start
    return x


class overload:
    """Simulates an overloaded function in C++.
    For example:
    ```
    @overload
    def function():
        pass
    @function.args(int)
    def _(a: int):
        print("got int: %i" % a)
    @function.args(a=int)
    def _(a: int = 0):
        print("got kwarg int: %i" % a)
    @function.args(int, int)
    def _(a: int, b: int):
        print("got int: %i and int: %i" % (a, b))
    function(1)       # -> "got int: 1"
    function(a=1)     # -> "got kwarg int: 1"
    function(1, 2)    # -> "got int: 1 and int: 2"
    function(1, b=2)  # -> TypeError("No matching overload function")
    ```
    """

    def __init__(self, f=None):
        self.cases = {}
        self.begin_function = f

    def args(self, *args, **kwargs):
        """Store a function with the given args as the function's arg
        types, and the given kwargs as the function's kwarg types.
        See examples in the class documentation.
        """
        def store_function(f):
            self.cases[args, tuple(kwargs.items())] = f
            return f
        return store_function

    def __call__(self, *args, **kwargs) -> Any:
        """Find the function to call from the given args and kwargs.

        Raises:
            `TypeError`: When the appropriate function is not found.

        Returns:
            `Any`: The return value of the function.
        """
        try:
            function = self.cases[
                tuple(type(arg) for arg in args),
                tuple(zip(kwargs.keys(), (type(_) for _ in kwargs.values())))
            ]
        except KeyError:
            raise TypeError('No matching overload function') from None
        return function(*args, **kwargs)

    @property
    def __doc__(self) -> str:
        return self.begin_function.__doc__


def to_fullwidth(c: str) -> str:
    """Changes the half-width character to full-width.

    Args:
        - `c` (`str`): The half-width character.

    Returns:
        - `str`: The full-width character.
    """
    if c == ' ':
        return chr(0x3000)
    if ord(c) > 0x7e or ord(c) < 0x21:
        return c
    return chr(ord(c) + 0xfee0)


def to_halfwidth(c: str) -> str:
    """Changes the full-width character to half-width.

    Args:
        - `c` (`str`): The full-width character.

    Returns:
        - `str`: The half-width character.
    """
    if ord(c) < 0xff01 or ord(c) > 0xff5e:
        return c
    return chr(ord(c) - 0xfee0)


def readchar(choices: Optional[str] = None, case_sensitive: bool = False, pr: bool = True) -> Optional[str]:
    try:
        import sys
        import termios
        import tty
    except ImportError:
        return ''

    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
    try:
        tty.setraw(fd)
        while True:
            ch = sys.stdin.read(1)
            if choices is None or ch in choices:
                ret = ch
                break
            if not case_sensitive and (ch.lower() in choices or ch.upper() in choices):
                ret = ch.lower() if ch.lower() in choices else ch.upper()
                break
            if ch == '\x03':
                ret = ch
                break
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    if pr:
        print(ch, end='', flush=True)
    return ret


def clear_screen() -> NoReturn:
    if sys.platform == 'win32':
        os.system('cls')
    elif sys.platform.startswith('linux'):
        os.system('clear')


def get_position() -> Tuple[int, int]:
    try:
        import sys
        import termios
        import tty
    except ImportError:
        return (-1, -1)

    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
    try:
        tty.setraw(fd)
        print('\x1b[6n', file=sys.stderr)
        n = ''
        c = ''
        while c != ';':
            c = sys.stdin.read(1)
            n += c
        n = int(n[:-1])
        n2 = ''
        while c != 'R':
            c = sys.stdin.read(1)
            n2 += c
        n2 = int(n2[:-1])
    except:
        raise
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    return (n, n2)


def numeral_input(start, stop, q=True, callback: Optional[Callable[[int], Any]] = None) -> int:
    if start > stop:
        start, stop = stop, start
    ret = ['0']
    print(0, end='', flush=True)
    if callback:
        callback(0)
    # digit = 0
    while True:
        c = readchar('1234567890\r\n\x7f\x08' + ('q' if q else ''), pr=False)
        if c in '\x7f\x08':
            if ret:
                ret = ret[:-1]
                print('\x1b[1D\x1b[K', end='', flush=True)
                if not ret:
                    ret = ['0']
                    print('0', end='', flush=True)
                if callback:
                    callback(int(''.join(ret)))
        elif c == '\x03':
            return -1
        elif c in '\r\n':
            if int(''.join(ret)) >= start:
                print()
                return int(''.join(ret))
        elif c == 'q':
            return -2
        else:
            ret.append(c)
            if int(''.join(ret)) >= stop or ret == ['0', '0']:
                ret = ret[:-1]
            elif ret[0] == '0' and len(ret) != 1:
                ret = [c]
                print('\x1b[1D' + c, end='', flush=True)
                if callback:
                    callback(int(c))
            else:
                print(ret[-1], end='', flush=True)
                if callback:
                    callback(int(''.join(ret)))
